using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Tetris.Blocks;
using Tetris.Grid;

namespace Tetris
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class TetrisGameFramework : Microsoft.Xna.Framework.Game
    {
        private const int GRID_WIDTH = 15;
        private const int GRID_HEIGHT = 26;
        private const int USER_MOVEMENT_DELAY_MS = 100;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        private Texture2D redTex, orangeTex, yellowTex, greenTex, blueTex, purpleTex, pinkTex;
        private TetrisBlock fallingBlock;
        private TetrisBlock nextBlock;
        private BlockGrid blockGrid;
        private GridPosition startingGridPosition;
        private int textureSize;
        private TimeSpan timeSinceLastUpdate;
        private TimeSpan timeSinceLastBlockMove;
        private Random random = new Random();
        private bool fallingBlockGracePeriod = false;
        
        private KeyboardState oldKeyboardState;

        /// <summary>
        /// TetrisGameFramework constructor; sets ups graphics manager and content root directory.
        /// </summary>
        public TetrisGameFramework()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            blockGrid = new BlockGrid(GRID_WIDTH, GRID_HEIGHT);

            int startingGridCol = blockGrid.width / 2;
            startingGridPosition = new GridPosition(startingGridCol, 0);
            timeSinceLastUpdate = TimeSpan.Zero;
            timeSinceLastBlockMove = TimeSpan.Zero;

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // load in all tetris block textures
            redTex = Content.Load<Texture2D>("img/square_red");
            orangeTex = Content.Load<Texture2D>("img/square_orange");
            yellowTex = Content.Load<Texture2D>("img/square_yellow");
            greenTex = Content.Load<Texture2D>("img/square_green");
            blueTex = Content.Load<Texture2D>("img/square_blue");
            purpleTex = Content.Load<Texture2D>("img/square_purple");
            pinkTex = Content.Load<Texture2D>("img/square_pink");

            textureSize = redTex.Width;
            if (redTex.Height != textureSize ||
                orangeTex.Width != textureSize ||
                orangeTex.Height != textureSize ||
                yellowTex.Width != textureSize ||
                yellowTex.Height != textureSize ||
                greenTex.Width != textureSize ||
                greenTex.Height != textureSize ||
                blueTex.Width != textureSize ||
                blueTex.Height != textureSize ||
                purpleTex.Width != textureSize ||
                purpleTex.Height != textureSize ||
                pinkTex.Width != textureSize ||
                pinkTex.Height != textureSize)
                throw new Exception("Texture sizes do not match!");

            graphics.PreferredBackBufferHeight = blockGrid.height * textureSize;
            graphics.PreferredBackBufferWidth = blockGrid.width * textureSize;
            graphics.ApplyChanges();

            fallingBlock = null;
            nextBlock = randomTetrisBlock();
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            timeSinceLastUpdate += gameTime.ElapsedGameTime;

            UpdateInput(gameTime);

            // a block is falling right now or the player has an extra opportunity to shift the block that fell previously
            if (fallingBlock != null)
            {
                if (timeSinceLastUpdate.Seconds > 0)
                {
                    // Update the falling tetris block
                    fallingBlock.Update(gameTime, blockGrid);

                    // this block can no longer move
                    if (!fallingBlock.CanMove(blockGrid, Block.MovementDirection.DOWN))
                    {
                        // we were previously in our grace period -- time to immobilize the block
                        if (fallingBlockGracePeriod)
                        {
                            fallingBlock.MakeBlocksStatic();
                            fallingBlock = null;
                            fallingBlockGracePeriod = false;
                        }
                        // allow for movement grace period
                        else
                        {
                            fallingBlockGracePeriod = true;
                        }
                    }

                    timeSinceLastUpdate = TimeSpan.Zero;
                }
            }
            // no block is currently falling; check for completed rows and then update all blocks on the grid
            else
            {
                for (int row = 0; row < blockGrid.height; row++)
                {
                    bool completeRow = true;
                    for (int col = 0; col < blockGrid.width; col++)
                    {
                        if (!blockGrid.ContainsStaticBlock(new GridPosition(col, row)))
                        {
                            completeRow = false;
                            break;
                        }
                    }

                    // handle complete row!
                    if (completeRow)
                    {
                        for (int col = 0; col < blockGrid.width; col++)
                        {
                            blockGrid.SetCell(new GridPosition(col, row), null);
                        }

                        // shift all rows above the deleted row down
                        for (int rowToShiftDown = row - 1; rowToShiftDown >= 0; rowToShiftDown--)
                        {
                            for (int col = 0; col < blockGrid.width; col++)
                            {
                                GridPosition gridPosition = new GridPosition(col, rowToShiftDown);
                                if (!blockGrid.IsEmpty(gridPosition))
                                {
                                    blockGrid.BlockAt(gridPosition).Move(blockGrid, Block.MovementDirection.DOWN);
                                }
                            }
                        }
                    }
                }

                // finally, generate the next Tetris block
                generateNextTetrisBlock();
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend);
            // Draw all tetris blocks.
            for (int row = 0; row < blockGrid.height; row++)
            {
                for (int col = 0; col < blockGrid.width; col++)
                {
                    GridPosition gridPosition = new GridPosition(col, row);
                    if (!blockGrid.IsEmpty(gridPosition))
                    {
                        Vector2 position = blockGrid.BlockAt(gridPosition).gridPosition.AsVector() * textureSize;
                        spriteBatch.Draw(blockGrid.BlockAt(gridPosition).texture, position, Color.White);
                    }
                }
            }
            spriteBatch.End();

            base.Draw(gameTime);
        }

        /// <summary>
        /// Drop a new falling block and set up the next block to be dropped.
        /// </summary>
        private void generateNextTetrisBlock()
        {
            fallingBlock = nextBlock;
            nextBlock = randomTetrisBlock();
        }

        /// <summary>
        /// Create a random new TetrisBlock.
        /// </summary>
        /// <returns>The new TetrisBlock.</returns>
        private TetrisBlock randomTetrisBlock()
        {
            TetrisBlock block = null;
            int randomNumber = random.Next(0, TetrisBlock.NumBlockTypes());
            switch(randomNumber)
            {
                case 0:
                    block = new Stick(startingGridPosition, pinkTex);
                    break;
                case 1:
                    block = new Square(startingGridPosition, yellowTex);
                    break;
                case 2:
                    block = new Tee(startingGridPosition, purpleTex);
                    break;
                case 3:
                    block = new ForwardL(startingGridPosition, blueTex);
                    break;
                case 4:
                    block = new BackwardL(startingGridPosition, orangeTex);
                    break;
                case 5:
                    block = new LeftZigZag(startingGridPosition, redTex);
                    break;
                case 6:
                    block = new RightZigZag(startingGridPosition, greenTex);
                    break;
                default:
                    string exceptionDescription = String.Format("Don't know how to create TetrisBlock for type #{0}", TetrisBlock.NumBlockTypes());
                    throw new ArgumentException(exceptionDescription);
            }

            return block;
        }

        /// <summary>
        /// Handle keyboard input.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        private void UpdateInput(GameTime gameTime)
        {
            timeSinceLastBlockMove += gameTime.ElapsedGameTime;

            KeyboardState newState = Keyboard.GetState();

            if (newState.IsKeyDown(Keys.Escape))
            {
                Exit();
            }

            // if there's no falling block, we don't need to update input.
            if (fallingBlock == null)
                return;

            // Is the space bar down?
            if (newState.IsKeyDown(Keys.Space))
            {
                // If not down last update, key has just been pressed.
                if (!oldKeyboardState.IsKeyDown(Keys.Space))
                {
                    // immediately drop the block
                    fallingBlock.ImmediateDrop(blockGrid);
                }
            }
            // Is the up arrow key down?
            else if (newState.IsKeyDown(Keys.Up))
            {
                // If not down last update, key has just been pressed.
                if (!oldKeyboardState.IsKeyDown(Keys.Up))
                {
                    // flip the block
                    fallingBlock.FlipOrientation(blockGrid);
                }
            }
            // Is the right arrow key down?
            else if (newState.IsKeyDown(Keys.Right))
            {
                // If the key was down last update, make sure enough time has passed to move the block in this direction again.
                if (!oldKeyboardState.IsKeyDown(Keys.Right) || timeSinceLastBlockMove.Milliseconds > USER_MOVEMENT_DELAY_MS)
                {
                    // move the block right
                    fallingBlock.Move(blockGrid, Block.MovementDirection.RIGHT);
                    timeSinceLastBlockMove = TimeSpan.Zero;
                }
            }
            // Is the left arrow key down?
            else if (newState.IsKeyDown(Keys.Left))
            {
                // If the key was down last update, make sure enough time has passed to move the block in this direction again.
                if (!oldKeyboardState.IsKeyDown(Keys.Left) || timeSinceLastBlockMove.Milliseconds > USER_MOVEMENT_DELAY_MS)
                {
                    // move the block left
                    fallingBlock.Move(blockGrid, Block.MovementDirection.LEFT);
                    timeSinceLastBlockMove = TimeSpan.Zero;
                }
            }
            // Is the down arrow key down?
            else if (newState.IsKeyDown(Keys.Down))
            {
                // If the key was down last update, make sure enough time has passed to move the block in this direction again.
                if (!oldKeyboardState.IsKeyDown(Keys.Down) || timeSinceLastBlockMove.Milliseconds > USER_MOVEMENT_DELAY_MS)
                {
                    // move the block down
                    fallingBlock.Move(blockGrid, Block.MovementDirection.DOWN);
                    timeSinceLastBlockMove = TimeSpan.Zero;
                }
            }

            // Update saved state.
            oldKeyboardState = newState;
        }
    }
}
